﻿/**
* ----------------------------- JSTORAGE -------------------------------------
* Simple local storage wrapper to save data on the browser side, supporting
* all major browsers - IE6+, Firefox2+, Safari4+, Chrome4+ and Opera 10.5+
*
* Copyright (c) 2010 Andris Reinman, andris.reinman@gmail.com
* Project homepage: www.jstorage.info
*
* Licensed under MIT-style license:
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/

/**
* USAGE:
*
* jStorage requires Prototype, MooTools or jQuery! If jQuery is used, then
* jQuery-JSON (http://code.google.com/p/jquery-json/) is also needed.
* (jQuery-JSON needs to be loaded BEFORE jStorage!)
*
* Methods:
*
* -set(key, value)
* $.jStorage.set(key, value) -> saves a value
*
* -get(key[, default])
* value = $.jStorage.get(key [, default]) ->
*    retrieves value if key exists, or default if it doesn't
*
* -deleteKey(key)
* $.jStorage.deleteKey(key) -> removes a key from the storage
*
* -flush()
* $.jStorage.flush() -> clears the cache
* 
* -storageObj()
* $.jStorage.storageObj() -> returns a read-ony copy of the actual storage
* 
* -storageSize()
* $.jStorage.storageSize() -> returns the size of the storage in bytes
*
* -index()
* $.jStorage.index() -> returns the used keys as an array
* 
* <value> can be any JSON-able value, including objects and arrays.
*
*/

(function ($) {
    if (!$ || !($.toJSON || Object.toJSON || window.JSON)) {
        throw new Error("jQuery, MooTools or Prototype needs to be loaded before jStorage!");
    }

    var 
    /* This is the object, that holds the cached values */
		_storage = {},

    /* Actual browser storage (localStorage or globalStorage['domain']) */
		_storage_service = { jStorage: "{}" },

    /* DOM element for older IE versions, holds userData behavior */
		_storage_elm = null,

    /* How much space does the storage take */
		_storage_size = 0,

    /* function to encode objects to JSON strings */
		json_encode = $.toJSON || Object.toJSON || (window.JSON && (JSON.encode || JSON.stringify)),

    /* function to decode objects from JSON strings */
		json_decode = $.evalJSON || (window.JSON && (JSON.decode || JSON.parse)) || function (str) {
		    return String(str).evalJSON();
		},

    /**
    * XML encoding and decoding as XML nodes can't be JSON'ized
    * XML nodes are encoded and decoded if the node is the value to be saved
    * but not if it's as a property of another object
    * Eg. -
    *   $.jStorage.set("key", xmlNode);        // IS OK
    *   $.jStorage.set("key", {xml: xmlNode}); // NOT OK
    */
		_XMLService = {

		    /**
		    * Validates a XML node to be XML
		    * based on jQuery.isXML function
		    */
		    isXML: function (elm) {
		        var documentElement = (elm ? elm.ownerDocument || elm : 0).documentElement;
		        return documentElement ? documentElement.nodeName !== "HTML" : false;
		    },

		    /**
		    * Encodes a XML node to string
		    * based on http://www.mercurytide.co.uk/news/article/issues-when-working-ajax/
		    */
		    encode: function (xmlNode) {
		        if (!this.isXML(xmlNode)) {
		            return false;
		        }
		        try { // Mozilla, Webkit, Opera
		            return new XMLSerializer().serializeToString(xmlNode);
		        } catch (E1) {
		            try {  // IE
		                return xmlNode.xml;
		            } catch (E2) { }
		        }
		        return false;
		    },

		    /**
		    * Decodes a XML node from string
		    * loosely based on http://outwestmedia.com/jquery-plugins/xmldom/
		    */
		    decode: function (xmlString) {
		        var dom_parser = ("DOMParser" in window && (new DOMParser()).parseFromString) ||
						(window.ActiveXObject && function (_xmlString) {
						    var xml_doc = new ActiveXObject('Microsoft.XMLDOM');
						    xml_doc.async = 'false';
						    xml_doc.loadXML(_xmlString);
						    return xml_doc;
						}),
				resultXML;
		        if (!dom_parser) {
		            return false;
		        }
		        resultXML = dom_parser.call("DOMParser" in window && (new DOMParser()) || window, xmlString, 'text/xml');
		        return this.isXML(resultXML) ? resultXML : false;
		    }
		};

    ////////////////////////// PRIVATE METHODS ////////////////////////

    /**
    * Initialization function. Detects if the browser supports DOM Storage
    * or userData behavior and behaves accordingly.
    * @returns undefined
    */
    function _init() {
        /* Check if browser supports localStorage */
        if (window.localStorage) {
            try {
                _storage_service = window.localStorage;
            } catch (E3) { /* Firefox fails when touching localStorage and cookies are disabled */ }
        }
        /* Check if browser supports globalStorage */
        else if (window.globalStorage) {
            try {
                _storage_service = window.globalStorage[window.location.hostname];
            } catch (E4) { /* Firefox fails when touching localStorage and cookies are disabled */ }
        }
        /* Check if browser supports userData behavior */
        else {
            _storage_elm = document.createElement('link');
            if (_storage_elm.addBehavior) {

                /* Use a DOM element to act as userData storage */
                _storage_elm.style.behavior = 'url(#default#userData)';

                /* userData element needs to be inserted into the DOM! */
                document.getElementsByTagName('head')[0].appendChild(_storage_elm);

                _storage_elm.load("jStorage");
                var data = "{}";
                try {
                    data = _storage_elm.getAttribute("jStorage");
                } catch (E5) { }
                _storage_service.jStorage = data;
            } else {
                _storage_elm = null;
                return;
            }
        }

        /* if jStorage string is retrieved, then decode it */
        if (_storage_service.jStorage) {
            try {
                _storage = json_decode(String(_storage_service.jStorage));
            } catch (E6) { _storage_service.jStorage = "{}"; }
        } else {
            _storage_service.jStorage = "{}";
        }
        _storage_size = _storage_service.jStorage ? String(_storage_service.jStorage).length : 0;
    }

    /**
    * This functions provides the "save" mechanism to store the jStorage object
    * @returns undefined
    */
    function _save() {
        try {
            _storage_service.jStorage = json_encode(_storage);
            // If userData is used as the storage engine, additional
            if (_storage_elm) {
                _storage_elm.setAttribute("jStorage", _storage_service.jStorage);
                _storage_elm.save("jStorage");
            }
            _storage_size = _storage_service.jStorage ? String(_storage_service.jStorage).length : 0;
        } catch (E7) { /* probably cache is full, nothing is saved this way*/ }
    }

    /**
    * Function checks if a key is set and is string or numberic
    */
    function _checkKey(key) {
        if (!key || (typeof key != "string" && typeof key != "number")) {
            throw new TypeError('Key name must be string or numeric');
        }
        return true;
    }

    ////////////////////////// PUBLIC INTERFACE /////////////////////////

    $.jStorage = {
        /* Version number */
        version: "0.1.4",

        /**
        * Sets a key's value.
        * 
        * @param {String} key - Key to set. If this value is not set or not
        *				a string an exception is raised.
        * @param value - Value to set. This can be any value that is JSON
        *				compatible (Numbers, Strings, Objects etc.).
        * @returns the used value
        */
        set: function (key, value) {
            _checkKey(key);
            if (_XMLService.isXML(value)) {
                value = { _is_xml: true, xml: _XMLService.encode(value) };
            }
            _storage[key] = value;
            _save();
            return value;
        },

        /**
        * Looks up a key in cache
        * 
        * @param {String} key - Key to look up.
        * @param {mixed} def - Default value to return, if key didn't exist.
        * @returns the key value, default value or <null>
        */
        get: function (key, def) {
            _checkKey(key);
            if (key in _storage) {
                if (typeof _storage[key] == "object" &&
						_storage[key]._is_xml &&
							_storage[key]._is_xml) {
                    return _XMLService.decode(_storage[key].xml);
                } else {
                    return _storage[key];
                }
            }
            return typeof (def) == 'undefined' ? null : def;
        },

        /**
        * Deletes a key from cache.
        * 
        * @param {String} key - Key to delete.
        * @returns true if key existed or false if it didn't
        */
        deleteKey: function (key) {
            _checkKey(key);
            if (key in _storage) {
                delete _storage[key];
                _save();
                return true;
            }
            return false;
        },

        /**
        * Deletes everything in cache.
        * 
        * @returns true
        */
        flush: function () {
            _storage = {};
            _save();
            /*
            * Just to be sure - andris9/jStorage#3
            */
            try {
                window.localStorage.clear();
            } catch (E8) { }
            return true;
        },

        /**
        * Returns a read-only copy of _storage
        * 
        * @returns Object
        */
        storageObj: function () {
            function F() { }
            F.prototype = _storage;
            return new F();
        },

        /**
        * Returns an index of all used keys as an array
        * ['key1', 'key2',..'keyN']
        * 
        * @returns Array
        */
        index: function () {
            var index = [], i;
            for (i in _storage) {
                if (_storage.hasOwnProperty(i)) {
                    index.push(i);
                }
            }
            return index;
        },

        /**
        * How much space in bytes does the storage take?
        * 
        * @returns Number
        */
        storageSize: function () {
            return _storage_size;
        }
    };

    // Initialize jStorage
    _init();

})(window.jQuery || window.$);